package com.acme.concurrent.pool.impl;

import com.acme.concurrent.pool.DataSourceException;
import com.acme.concurrent.pool.DisposableDataSource;
import com.acme.concurrent.pool.NonBlockingPool;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SelfRegulatingSemaphorePool<T> implements NonBlockingPool<T>, AutoCloseable{
    private final Semaphore semaphore;
    private final Map<T, Boolean> storage;
    private final Lock lock = new ReentrantLock();
    private final int min;
    private final int max;
    private volatile int currentlyInUse;
    private volatile int currentlyAllocated;
    private final Runnable cleaner = new Cleaner();
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final DisposableDataSource<T> dataSource;

    public SelfRegulatingSemaphorePool(DisposableDataSource<T> dataSource,int min, int max) throws DataSourceException {
        this.max = max;
        this.min = min;
        this.dataSource = dataSource;
        currentlyAllocated = min;
        currentlyInUse = 0;
        semaphore = new Semaphore(max);
        storage = new HashMap<T, Boolean>(max);
        scheduler.scheduleWithFixedDelay(cleaner, 0, Cleaner.CLEANER_INTERVAL_SECONDS, TimeUnit.SECONDS);
        for(int i = 0; i < currentlyAllocated; i++){
            storage.put(dataSource.newInstance(), true);
        }
        if (storage.size() != currentlyAllocated) {
            throw new RuntimeException("DataSource created two or more similar objects");
        }
    }

    public SelfRegulatingSemaphorePool(DisposableDataSource<T> dataSource, int max) throws DataSourceException {
        this(dataSource, 0, max);
    }

    @Override
    public T get() throws InterruptedException {
        semaphore.acquire();
        return getNextFreeObject();
    }

    @Override
    public void release(T object) {
        if(putObject(object))
            semaphore.release();
    }

    @Override
    public T tryGet() {
        if(semaphore.tryAcquire()){
            return getNextFreeObject();
        }else{
            return null;
        }

    }

    public int getCurrentlyAllocated() {
        return currentlyAllocated;
    }

    @Override
    public T tryGet(long timeout, TimeUnit timeUnit) throws InterruptedException {
        if(semaphore.tryAcquire(timeout, timeUnit)){
            return getNextFreeObject();
        }else{
            return null;
        }
    }

    private T getNextFreeObject(){
        try{
            lock.lock();
            checkEnoughAllocated();
            for(Map.Entry<T, Boolean> entry: storage.entrySet()){
                if(entry.getValue()){
                    entry.setValue(false);
                    currentlyInUse++;
                    return entry.getKey();
                }
            }
            throw new RuntimeException("Ooops, you should never get there");
        }finally {
            lock.unlock();
        }
    }

    private boolean putObject(T object){
        try{
            lock.lock();
            if(storage.containsKey(object)){
                currentlyInUse--;
                return !storage.put(object, true);
            }
            return false;
        }finally {
            lock.unlock();
        }
    }

    private void checkEnoughAllocated(){
        try {
            if(currentlyAllocated <= currentlyInUse && currentlyAllocated < max){
                storage.put(dataSource.newInstance(), true);
                currentlyAllocated++;
            }
        } catch (DataSourceException e) {
            throw new RuntimeException("Could not allocate object!", e);
        }

    }

    @Override
    public void close(){
        scheduler.shutdownNow();
    }

    class Cleaner implements Runnable{

        public static final int CLEANER_INTERVAL_SECONDS = 20;
        public static final int CLEAN_LEAVE = 1;

        @Override
        public void run() {
            try{
                lock.lock();
                System.out.println("Running cleaner...");
                Iterator<Map.Entry<T, Boolean>> iterator = storage.entrySet().iterator();
                Map.Entry<T, Boolean> entry;
                while(iterator.hasNext()){
                    entry = iterator.next();
                    if(currentlyAllocated <= Math.max(min, currentlyInUse + CLEAN_LEAVE)){
                        break;
                    }
                    if(entry.getValue()){
                        T toDispose = entry.getKey();
                        iterator.remove();
                        currentlyAllocated--;
                        try {
                            dataSource.dispose(toDispose);
                        } catch (DataSourceException e) {
                            System.err.println("Cleaner: Some error occurred while disposing object!");
                            e.printStackTrace();
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }

}